/*
 * ListManager.java
 */

package model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import exceptions.UPException;
import file.XMLFacade;

/**
 * ListManager is the God class for reading, storing, and accessing all data for
 * the UrbanParks model.
 * 
 * @author Sean Brown
 * @author Carlos Consolacion
 * @version 1.0
 */
public class ListManager {

	/**
	 * Map with User values and Integer keys (that belong to the User).
	 */
	private Map<Integer, User> my_user_map;

	/**
	 * Map with Job values and Integer keys (that belong to the Job).
	 */
	private Map<Integer, Job> my_job_map;

	/**
	 * Map with Park values and Integer keys (that belong to the Job).
	 */
	private Map<Integer, Park> my_park_map;

	/**
	 * Total number of Job objects allowed in the Map.
	 */
	private int my_max_total_jobs;

	/**
	 * Total number of Job objects allowed per week.
	 */
	private int my_max_jobs_per_week;

	/**
	 * Indicator that the User Map has been modified.
	 */
	private boolean my_users_are_mod;

	/**
	 * Indicator that the Job Map has been modified.
	 */
	private boolean my_jobs_are_mod;

	/**
	 * Indicator that the Park Map has been modified.
	 */
	private boolean my_parks_are_mod;

	/**
	 * The current user for the Urban Parks program.
	 */
	private User my_current_user;

	/**
	 * Constructor for ListManager instantiates fields for the class.
	 */
	public ListManager() {
		my_user_map = new HashMap<Integer, User>();
		my_job_map = new HashMap<Integer, Job>();
		my_park_map = new HashMap<Integer, Park>();
		my_users_are_mod = false;
		my_jobs_are_mod = false;
		my_parks_are_mod = false;
	}

	/**
	 * Attempts to add a Job to the field for the Map of Jobs (along with the ID
	 * of the Job as a key to the Map.) If successful, sets the indicator that
	 * the map has been modified.
	 * 
	 * @assumes the_job is non-null.
	 * @param the_job
	 *            The job to be added.
	 * @throws UPException
	 *             When the Job being added is not a valid job (per the business
	 *             rules).
	 */
	public void addJob(final Job the_job) throws UPException {
		validateJob(the_job); // throws UPException
		my_job_map.put(the_job.getID(), the_job);
		my_jobs_are_mod = true;
	}

	/**
	 * Returns the total number of Job objects currently in the Map.
	 * 
	 * @assumes my_job_map is non-null.
	 * @return int value for the number of jobs currently.
	 */
	public int getTotalJobCount() {
		return my_job_map.values().size();
	}

	/**
	 * Returns the max number of jobs currently allowed.
	 * 
	 * @assumes my_max_total_jobs is non-null and non-negative.
	 * @return The max number of jobs allowed.
	 */
	public int maxJobs() {
		return my_max_total_jobs;
	}

	/**
	 * Returns the max total jobs per week (7 days)
	 * 
	 * @assumes my_max_jobs_per_week is non-null and non-negative.
	 * @return The max total jobs allowed per week.
	 */
	public int maxJobsPerWeek() {
		return my_max_jobs_per_week;
	}

	/**
	 * Changes my_current_user to the User with an ID that matches. Null
	 * parameter will set current user to no user (in case of logout).
	 * 
	 * @param the_user
	 *            The new current user.
	 */
	public void switchUser(final User the_user) {
		my_current_user = the_user;
	}

	/**
	 * Volunteers the current use for a job with the id passed in the workload
	 * category passed (0 = light, 1 = med, 2 = heavy).
	 * 
	 * @param the_job_id
	 *            The ID of the Job to volunteer for.
	 * @param the_workload
	 *            The workload to volunteer for.
	 * @throws UPException
	 *             If the current user cannot volunteer for this Job for one of
	 *             the following reasons:
	 *             *They are already signed up for that job
	 *             *The job is in the past.
	 *             *They are already signed up for a job on the day of the job requested.
	 *             *The workload category requested is full or invalid
	 *             *The user is not logged in as a Volunteer type.
	 */
	public void volunteerForJob(final int the_job_id, final int the_workload)
			throws UPException {
		if (my_current_user.getRole() == Role.VOLUNTEER) {
			final Job j = findJob(the_job_id);
			if (j.getVolunteerIDs().contains(my_current_user.getID())) {
				throw new UPException("You are already signed up for this job.");
			}

			// Business Rule 6
			if (j.getStartTime().before(new GregorianCalendar()) || 
					j.getEndTime().before((new GregorianCalendar()))) {
				throw new UPException("You cannot sign up for a job that has passed.");
			}

			// Business Rule 7
			Collection<Job> current_user_jobs = getUserJobs();
			for (Job current_job : current_user_jobs) {
				if (current_job.getStartTime().get(GregorianCalendar.DAY_OF_MONTH)
						== j.getStartTime().get(GregorianCalendar.DAY_OF_MONTH) &&
						current_job.getStartTime().get(GregorianCalendar.YEAR) == 
						+ j.getStartTime().get(GregorianCalendar.YEAR)) {
					throw new UPException(
							"You cannot sign up for more than one job on the same day.");
				}
			}

			switch (the_workload) {
			case 0:
				if (j.getLightVolunteersNeeded() != 0) {
					j.addVolunteer(my_current_user.getID(), the_workload);
					my_jobs_are_mod = true;
				} else {
					throw new UPException(
							"No light workers are needed for this job.");
				}
				break;
			case 1:
				if (j.getMedVolunteersNeeded() != 0) {
					j.addVolunteer(my_current_user.getID(), the_workload);
					my_jobs_are_mod = true;
				} else {
					throw new UPException(
							"No medium workers are needed for this job.");
				}
				break;
			case 2:
				if (j.getHeavyVolunteersNeeded() != 0) {
					j.addVolunteer(my_current_user.getID(), the_workload);
					my_jobs_are_mod = true;
				} else {
					throw new UPException(
							"No heavy workers are needed for this job.");
				}
				break;
			default:
				throw new UPException(
						"The workload category chosen is invalid.");
			}
		} else {
			throw new UPException("You are not logged in as a volunteer."
					+ " Please login as a volunteer to sign up for a job.");
		}
	}

	/**
	 * Blackballs the user with the ID value that matches the integer passed.
	 * 
	 * @param the_id
	 *            the ID value for the User to blackball.
	 * @throws UPException
	 *             If the user is not a volunteer type or the user is already
	 *             blackballed.
	 */
	public void blackballUser(int the_id) throws UPException {

		if (my_current_user.getRole() != Role.STAFFMEMBER) {
			throw new UPException("Only Staff Members can blackball users.");
		}
		User u = findUser(the_id);
		if (u.getRole() != Role.VOLUNTEER) {
			throw new UPException("Only volunteers can be blackballed.");
		}
		if (u.isBlackballed()) {
			throw new UPException(u.getName() + " is already blackballed.");
		}
		u.setBlackballed(true);
		my_users_are_mod = true;

	}

	/**
	 * Returns my_current_user, the User who currently is logged into the
	 * program.
	 * 
	 * @assumes my_current_user is non-null. MAKE SURE A USER IS LOGGED IN.
	 * @return User the current user.
	 */
	public User getCurrentUser() {
		return my_current_user;
	}

	/**
	 * Returns a reference to the User who has the ID value that is passed.
	 * 
	 * @param the_id
	 *            The ID value for the user.
	 * @return The User with the matching ID based on email hashCode.
	 * @throws UPException
	 *             When the ID matches no User in the Map.
	 */
	public User findUser(final int the_id) throws UPException {
		final User a_user = my_user_map.get(the_id);
		if (a_user != null) {
			return a_user;
		} else {
			throw new UPException("The user id " + the_id + " is invalid.");
		}
	}

	/**
	 * Returns a reference to the Job that has the ID value that is passed.
	 * 
	 * @param the_id
	 *            The ID for the Job to find.
	 * @return The Job with the matching ID.
	 * @throws UPException
	 *             When the ID matches no Job in the Map.
	 */
	public Job findJob(final int the_id) throws UPException {
		final Job a_job = my_job_map.get(the_id);
		if (a_job != null) {
			return a_job;
		} else {
			throw new UPException("The job id " + the_id + " is invalid.");
		}
	}

	/**
	 * Returns a reference to the Park that has the ID value that is passed.
	 * 
	 * @param the_id
	 *            The ID for the Park to find.
	 * @return The Park with the matching ID.
	 * @throws UPException
	 *             When the ID matches no Park in the Map.
	 */
	public Park findPark(final int the_id) throws UPException {
		final Park a_park = my_park_map.get(the_id);
		if (a_park != null) {
			return a_park;
		} else {
			throw new UPException("The park id " + the_id + " is invalid.");
		}
	}

	/**
	 * Returns an immutable list of all users in the user map. The list is
	 * sorted using the UserComparator.
	 * 
	 * @return List of all Users in the map.
	 */
	public List<User> getUsers() {
		final List<User> users = new LinkedList<User>();
		for (User u : my_user_map.values()) {
			users.add((User) u.clone());
		}
		return users;
	}

	/**
	 * Returns an immutable list of all volunteers in the user map. The list is
	 * sorted using the UserComparator.
	 * 
	 * @return Sorted List of all Users in the map.
	 */
	public List<User> getVolunteers() {
		final List<User> users = new LinkedList<User>();
		for (User u : my_user_map.values()) {
			if (u.getRole() == Role.VOLUNTEER) {
				users.add((User) u.clone());
			}
		}
		Collections.sort(users, new UserComparator());
		return users;
	}

	/**
	 * Returns an immutable list of all parks in the park map.
	 * 
	 * @return List of all Parks in the map.
	 */
	public List<Park> getParks() {
		final List<Park> parks = new LinkedList<Park>();
		for (Park p : my_park_map.values()) {
			parks.add((Park) p.clone());
		}
		return parks;
	}

	/**
	 * Returns an immutable list of all jobs in the job map.
	 * 
	 * @return List of all Jobs in the map.
	 */
	public List<Job> getJobs() {
		final List<Job> jobs = new LinkedList<Job>();
		for (Job j : my_job_map.values()) {
			jobs.add((Job) j.clone());
		}
		return jobs;
	}

	/**
	 * Returns a list of Jobs (immutable) that begin on the same date passed as
	 * a parameter.
	 * 
	 * @param the_date
	 *            The date to find jobs for.
	 * @return A List of Jobs that are on the given date.
	 */
	public List<Job> getJobsOn(final GregorianCalendar the_date) {
		final List<Job> jobs = new LinkedList<Job>();
		if (my_current_user.getRole().equals(Role.STAFFMEMBER)) {
			for (Job j : my_job_map.values()) {
				final GregorianCalendar start_date = j.getStartTime();
				if (start_date.get(GregorianCalendar.YEAR) == the_date
						.get(GregorianCalendar.YEAR)
						&& start_date.get(GregorianCalendar.MONTH) == the_date
								.get(GregorianCalendar.MONTH)
						&& start_date.get(GregorianCalendar.DAY_OF_MONTH) == the_date
								.get(GregorianCalendar.DAY_OF_MONTH)) {
					jobs.add((Job) j.clone());
				}
			}
		}
		return jobs;
	}

	/**
	 * Returns a List of Jobs that the current user is signed up for. Returns an
	 * empty List if the current user is not a Volunteer. Jobs returned are
	 * immutable.
	 * 
	 * @assumes current_user is non-null.
	 * @return List of Jobs that contain the ID value of the current user.
	 */
	public List<Job> getUserJobs() {
		final List<Job> jobs = new LinkedList<Job>();
		if (my_current_user.getRole().equals(Role.VOLUNTEER)) {
			for (Job j : my_job_map.values()) {
				if (j.getVolunteerIDs().contains(
						(Integer) my_current_user.getID())) {
					jobs.add((Job) j.clone());
				}
			}
		}
		return jobs;
	}

	/**
	 * Returns a List of Jobs (immutable) that take place at the Park which
	 * contains the id passed as a parameter. Returns an empty List if no Park
	 * matches the ID or the Park with the matching ID has no Jobs.
	 * 
	 * @param the_park_id
	 *            The ID of the Park to find
	 * @return List of Jobs at the Park with the id passed.
	 */
	public List<Job> getParkJobs(final int the_park_id) {
		final List<Job> jobs = new LinkedList<Job>();
		for (Job j : my_job_map.values()) {
			if (j.getParkId() == the_park_id) {
				jobs.add((Job) j.clone());
			}
		}
		return jobs;
	}

	/**
	 * Returns a List of Park (immutable) that the current user manages. Returns
	 * an empty List if the current user is not a Park Manager or the current
	 * user does not manage any parks.
	 * 
	 * @return
	 */
	public List<Park> getParksManaged() {
		final List<Park> parks = new ArrayList<Park>();
		if (my_current_user.getRole().equals(Role.PARKMANAGER)) {
			for (Park p : my_park_map.values()) {
				// If the current user manages this park
				if (my_current_user.getID() == p.getManagerID()) {
					parks.add((Park) p.clone());
				}
			}
		}
		return parks;
	}

	/**
	 * Returns a List of Users (immutable) that are blackballed. Will return an
	 * empty List if there are no blackballed users.
	 * 
	 * @return List of Users that are blackballed.
	 */
	public List<User> getBlackballed() {
		final List<User> blackballed = new LinkedList<User>();
		if (my_current_user.getRole() == Role.STAFFMEMBER) {
			for (User u : my_user_map.values()) {
				if (u.isBlackballed() && u.getRole().equals(Role.VOLUNTEER)) {
					blackballed.add((User) u.clone());
				}
			}
		}
		return blackballed;
	}

	/**
	 * Takes a Job object and verifies that it passes all of the business rules
	 * proposed in the requirements. Throws an exception otherwise.
	 * 
	 * @assumes the_job is non-null.
	 * @param the_job
	 *            The Job to verify.
	 * @throws UPException
	 *             When: *The Job is already in the Map *The max number of Jobs
	 *             has been reached. *The start date falls after the end date.
	 *             *The Job is in the past. *The Job lasts more than two days.
	 *             *The Job is more than three months (90 days) in the future.
	 *             *The number of Jobs in the week that this Job falls on (3
	 *             days surrounding this job on either side) is more than the
	 *             maximum jobs allowed per week.
	 */
	private void validateJob(final Job the_job) throws UPException {
		if (my_job_map.containsKey(the_job.getID())) {
			throw new UPException("That job already exists.");
		}

		if (my_job_map.size() >= my_max_total_jobs) {
			throw new UPException("Max job count (" + my_max_total_jobs
					+ ") has been reached.");
		}

		final GregorianCalendar start_date = the_job.getStartTime();
		final GregorianCalendar end_date = the_job.getEndTime();
		if (start_date.after(end_date)) {
			throw new UPException("The start date of this job falls after the"
					+ " end date.");
		}

		if (start_date.before(new GregorianCalendar())) {
			throw new UPException(
					"The job cannot be added that is in the past.");
		}

		final GregorianCalendar two_after = (GregorianCalendar) start_date
				.clone();
		two_after.add(GregorianCalendar.DATE, 2);
		if (two_after.before(end_date)) {
			throw new UPException("A job cannot last more than two days.");
		}

		final GregorianCalendar three_months_later = new GregorianCalendar();
		three_months_later.add(GregorianCalendar.DATE, 90);
		if (start_date.after(three_months_later)) {
			throw new UPException("A job may not be added that is more than "
					+ "three months in the future.");
		}

		int week_count = 0;
		final GregorianCalendar three_before = (GregorianCalendar) start_date
				.clone();
		three_before.add(GregorianCalendar.DATE, -3);
		final GregorianCalendar three_after = (GregorianCalendar) start_date
				.clone();
		three_after.add(GregorianCalendar.DATE, 3);
		for (Job j : my_job_map.values()) {
			if (j.getStartTime().after(three_before)
					&& j.getStartTime().before(three_after)) {
				week_count++;
				if (week_count >= my_max_jobs_per_week) {
					throw new UPException(
							"Maximum number of jobs for this week ("
									+ my_max_jobs_per_week
									+ ") has been reached.");
				}
			}
		}
	}

	/**
	 * Imports all data from XML file by creating an instance of XMLFacade and
	 * populating the HashMaps with the data from the facade.
	 * 
	 * @throws UPException
	 *             When the data cannot be loaded from the facade or the data
	 *             loaded is inaccurate.
	 */
	public void importData() throws UPException {

		final XMLFacade xml_import = new XMLFacade();
		final int[] temp;
		final Collection<User> user_list;
		final Collection<Job> job_list;
		final Collection<Park> park_list;

		// All can throw UPException
		user_list = xml_import.loadUsers();
		job_list = xml_import.loadJobs();
		park_list = xml_import.loadParks();
		temp = xml_import.loadConfig();

		if (temp != null && temp.length == 2) {
			my_max_total_jobs = temp[0];
			my_max_jobs_per_week = temp[1];
		} else {
			throw new UPException("Configuration data not loaded.");
		}
		if (user_list != null && job_list != null && park_list != null) {
			// No duplicate User/Job/Park is allowed by Map
			for (User u : user_list) {
				// Create map from UserID to User
				my_user_map.put(u.getID(), u);
			}
			for (Park p : park_list) {
				// Create map from ParkID to Park
				my_park_map.put(p.getID(), p);
			}
			for (Job j : job_list) {
				// Create map from JobID to Job
				my_job_map.put(j.getID(), j);
			}
		}
	}

	/**
	 * Exports all data in Maps (Park, Job, User) to XML by creating an instance
	 * of XMLFacade and passing collections of values from Maps to the facade.
	 * 
	 * @throws UPException
	 *             When the save has failed in XMLFacade.
	 */
	public void exportData() throws UPException {
		final XMLFacade xml_export = new XMLFacade();
		// All throw UPException.
		if (my_users_are_mod) {
			xml_export.saveUsers(my_user_map.values());
		}
		if (my_jobs_are_mod) {
			xml_export.saveJobs(my_job_map.values());
		}
		if (my_parks_are_mod) {
			xml_export.saveParks(my_park_map.values());
		}
	}
}